/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simhya.matheval;
import simhya.matheval.function.*;
import simhya.matheval.operator.*;
import java.util.HashMap;
import java.util.ArrayList;
import simhya.matheval.parser.*;


/**
 *
 * @author Luca
 */
public class Evaluator {
    private HashMap<String,FunctionDefinition> functionLookupTable;
    private HashMap<String,OperatorDefinition> operatorLookupTable;
    private HashMap<String,ExpressionNode> expressionNodeLookupTable;
    private ArrayList<Expression> expressionLookupTable;
    int evaluationCode;
    private SymbolArray variables;
    private SymbolArray constants;
    private MathParser parser;
    
    

    public Evaluator() {
        this.functionLookupTable = new HashMap<String,FunctionDefinition>();
        this.operatorLookupTable = new HashMap<String,OperatorDefinition>();
        this.expressionNodeLookupTable = new HashMap<String,ExpressionNode>();
        this.expressionLookupTable = new ArrayList<Expression>();
        this.variables = new SymbolArray();
        this.constants = new SymbolArray();
        evaluationCode = 0;
        registerOperators();
        registerBuiltinFunctions();
        parser = new MathParser(this);
    }


    public int parse(String expression) throws ParseException, TokenMgrError, NumberFormatException{
        Expression exp = parser.parse(expression);
        int id = -1;
        if (exp != null) 
            id = this.addExpression(exp);
        return id;
    }

    public Expression getExpression(int id) {
        if (id <0 || id >= this.expressionLookupTable.size())
            throw new EvalException("There is no expression with token " + id);
        return this.expressionLookupTable.get(id);
    }

    

    /**
     * registers builtin functions in the function table;
     */
    private void registerBuiltinFunctions() {
        functionLookupTable.put("abs", new Abs());
        functionLookupTable.put("acos", new Acos());
        functionLookupTable.put("asin", new Asin());
        functionLookupTable.put("atan", new Atan());
        functionLookupTable.put("cbrt", new Cbrt());
        functionLookupTable.put("ceil", new Ceil());
        functionLookupTable.put("cos", new Cos());
        functionLookupTable.put("cosh", new Cosh());
        functionLookupTable.put("exp", new Exp());
        functionLookupTable.put("floor", new Floor());
        functionLookupTable.put("log", new Log());
        functionLookupTable.put("log10", new Log10());
        functionLookupTable.put("log2", new Log2());
        functionLookupTable.put("max", new Max());
        functionLookupTable.put("min", new Min());
        functionLookupTable.put("randexp", new RandExp());
        functionLookupTable.put("randnorm", new RandNorm());
        functionLookupTable.put("sign", new Sign());
        functionLookupTable.put("sin", new Sin());
        functionLookupTable.put("sinh", new Sinh());
        functionLookupTable.put("sqrt", new Sqrt());
        functionLookupTable.put("tanh", new Tanh());
        functionLookupTable.put("tan", new Tan());
        functionLookupTable.put("uniform", new Uniform());
    }
    /**
     * Registers operators in the operator lookup table;
     */
    private void registerOperators() {
        this.operatorLookupTable.put("+", new Plus());
        this.operatorLookupTable.put("-", new Minus());
        this.operatorLookupTable.put("*", new Multiply());
        this.operatorLookupTable.put("/", new Divide());
        this.operatorLookupTable.put("%", new Mod());
        this.operatorLookupTable.put("^", new Power());
        this.operatorLookupTable.put("&&", new And());
        this.operatorLookupTable.put("||", new Or());
        this.operatorLookupTable.put("!", new Not());
        this.operatorLookupTable.put("==", new Equal());
        this.operatorLookupTable.put("!=", new NotEqual());
        this.operatorLookupTable.put("<", new Less());
        this.operatorLookupTable.put("<=", new LessOrEqual());
        this.operatorLookupTable.put(">", new Greater());
        this.operatorLookupTable.put(">=", new GreaterOrEqual());
        this.operatorLookupTable.put("plus", new Plus());
        this.operatorLookupTable.put("minus", new Minus());
        this.operatorLookupTable.put("multiply", new Multiply());
        this.operatorLookupTable.put("divide", new Divide());
        this.operatorLookupTable.put("mod", new Mod());
        this.operatorLookupTable.put("power", new Power());
        this.operatorLookupTable.put("and", new And());
        this.operatorLookupTable.put("or", new Or());
        this.operatorLookupTable.put("not", new Not());
        this.operatorLookupTable.put("equal", new Equal());
        this.operatorLookupTable.put("notequal", new NotEqual());
        this.operatorLookupTable.put("less", new Less());
        this.operatorLookupTable.put("lessorequal", new LessOrEqual());
        this.operatorLookupTable.put("greater", new Greater());
        this.operatorLookupTable.put("greaterorequal", new GreaterOrEqual());
    }

    public int addVariable(String name, double value) {
        if (variables.symbolLookupTable.containsKey(name))
            throw new EvalException("Variable " + name + " already defined.");
        int id = variables.addSymbol(name, value);
        return id;
    }

    public int addConstant(String name, double value) {
        if (constants.symbolLookupTable.containsKey(name))
            throw new EvalException("Constant " + name + " already defined.");
        int id = constants.addSymbol(name, value);
        return id;
    }

    public void registerFunction(String name, FunctionDefinition func) {
        if (this.functionLookupTable.containsKey(name) || name.equals("if"))
            throw new EvalException("There is already a function with name " + name);
        this.functionLookupTable.put(name, func);
    }

    public boolean isVariable(String name) {
        return variables.symbolLookupTable.containsKey(name);
    }


    public boolean isConstant(String name) {
        return constants.symbolLookupTable.containsKey(name);
    }

    public boolean isFunction(String name) {
        return name.equals("if") || this.functionLookupTable.containsKey(name);
    }

    public int getVariableCode(String name) {
        return variables.symbolLookupTable.get(name);
    }

    public int getConstantCode(String name) {
        return constants.symbolLookupTable.get(name);
    }



    public SymbolArray getVariableReference() {
        return this.variables;
    }

    public SymbolArray getConstantReference() {
        return this.constants;
    }



    /**
     * Sets the value of the variable with specified id
     * @param id the id of the variable
     * @param value a new double value
     * @throws {@link EvalException} if there is no variable with specified id
     */
    public void setVariableValue(int id, double value) {
        if (id >= variables.numberOfSymbols || id < 0)
            throw new EvalException("There is no variable with id " + id);
        variables.values[id] = value;
    }
    /**
     * Sets the value of the variable with specified name
     * @param name the name of the variable
     * @param value a new double value
     * @throws {@link EvalException} if there is no variable with specified id
     */
    public void setVariableValue(String name, double value) {
        Integer id = variables.symbolLookupTable.get(name);
        if (id == null)
            throw new EvalException("Variable " + name + " undefined.");
        variables.values[id] = value;
    }
    /**
     * Sets the value of the constant with specified id
     * @param id the id of the constant
     * @param value a new double value
     * @throws {@link EvalException} if there is no constant with specified id
     */
    public void setConstantValue(int id, double value) {
        if (id >= constants.numberOfSymbols || id < 0)
            throw new EvalException("There is no constant with id " + id);
        constants.values[id] = value;
    }
    /**
     * Sets the value of the constant with specified name
     * @param name the name of the constant
     * @param value a new double value
     * @throws {@link EvalException} if there is no constant with specified id
     */
    public void setConstantValue(String name, double value) {
        Integer id = constants.symbolLookupTable.get(name);
        if (id == null)
            throw new EvalException("Constant " + name + " undefined.");
        constants.values[id] = value;
    }
    /**
     * Returns the current value of the variable with specified id.
     * @param id the integer code of the variable
     * @return a double value
     * @throws {@link EvalException} if there is no variable with specified id
     */
    public double getVariableValue(int id) {
         if (id >= variables.numberOfSymbols || id < 0)
            throw new EvalException("There is no variable with id " + id);
        return variables.values[id];
    }
    /**
     * Returns the current value of the specified variable
     * @param name the name of the variable
     * @return a double value
     * @throws {@link EvalException} if the variable name is not defined
     */
    public double getVariableValue(String name) {
        Integer id = variables.symbolLookupTable.get(name);
        if (id == null)
            throw new EvalException("Variable " + name + " undefined.");
        return variables.values[id];
    }
    /**
     * Returns the current value of the constant with specified id.
     * @param id the integer code of the constant
     * @return a double value
     * @throws {@link EvalException} if there is no constant with specified id
     */
    public double getConstantValue(int id) {
        if (id >= constants.numberOfSymbols || id < 0)
            throw new EvalException("There is no constant with id " + id);
        return constants.values[id];
    }
    /**
     * Returns the current value of the specified constant
     * @param name the name of the constant
     * @return a double value
     * @throws {@link EvalException} if the constant name is not defined
     */
    public double getConstantValue(String name) {
        Integer id = constants.symbolLookupTable.get(name);
        if (id == null)
            throw new EvalException("Constant " + name + " undefined.");
        return constants.values[id];
    }

    public String getVariableName(int id) {
        if (id >= variables.numberOfSymbols || id < 0)
            throw new EvalException("There is no variable with id " + id);
        return variables.names.get(id);
    }

    public String getConstantName(int id) {
        if (id >= constants.numberOfSymbols || id < 0)
            throw new EvalException("There is no variable with id " + id);
        return constants.names.get(id);
    }
    /**
     * Returns an arraylist containing the name of all variables
     * @return
     */
    public ArrayList<String> getNameOfAllVariables() {
        return (ArrayList<String>)variables.names.clone();
    }
    /**
     * Returns an arraylist containing the name of all constants
     * @return
     */
    public ArrayList<String> getNameOfAllConstants() {
        return (ArrayList<String>)constants.names.clone();
    }


    /**
     * gets the current evaluation code
     * @return
     */
    public int getEvaluationCode() {
        return this.evaluationCode;
    }

    /**
     * changes the evaluation code and returns the new one.
     * @return
     */
    public int newEvaluationCode() {
        this.evaluationCode++;
        return this.evaluationCode;
    }


    /**
     * Checks if an expression node has already been defined. This is done by comparing
     * the whole expression below the node. If so, returns the equivalent node, otherwise
     * returns null. Otherwise, it adds the new node to the lookup table and returns it.
     * @param node
     * @return
     */
    public ExpressionNode checkNodeDefinition(ExpressionNode node) {
        String expression = node.getExpressionString(false);
        ExpressionNode n = this.expressionNodeLookupTable.get(expression);
        if (n != null)
            return n;
        else {
            this.expressionNodeLookupTable.put(expression, node);
            return node;
        }
    }




    public FunctionDefinition getFunctionDefinition(String name) {
        FunctionDefinition f = this.functionLookupTable.get(name);
        if (f == null)
            throw new EvalException("Function " + name + " is not defined.");
        return f;
    }

    public OperatorDefinition getOperatorDefinition(String name) {
        OperatorDefinition op = this.operatorLookupTable.get(name);
        if (op == null)
            throw new EvalException("Operator " + name + " is not defined.");
        return op;
    }


    private int addExpression(Expression e) {
        int id = this.expressionLookupTable.size();
        this.expressionLookupTable.add(e);
        e.setId(id);
        return id;
    }

}
